package br.com.massai.model.dao;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaQuery;

import org.hibernate.Criteria;
import org.hibernate.PropertyValueException;
import org.hibernate.Session;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.MatchMode;
import org.hibernate.exception.ConstraintViolationException;

import br.com.massai.controller.interceptor.LogInterceptor;
import br.com.massai.model.exception.SystemException;
import br.com.massai.view.enums.CoreError;

@Stateless
@SuppressWarnings({ "all" })
public class GenericDao<T> implements IGenericDao<T> {

	private static final long serialVersionUID = 848358960940476989L;

	protected static final String WHERE = "where";
	protected static final String AND = "and";

	private Class<T> entity;
	protected Map<String, Object> parameters;
	protected StringBuilder clauseWhere;

	public GenericDao() {
		super();
	}

	@PersistenceContext(unitName = IGenericDao.PERSISTENCE_UNIT)
	protected EntityManager em;

	@Override
	@Interceptors({ LogInterceptor.class })
	public T create(T entity) throws SystemException {
		checkDuplicate(entity);
		try{
			em.persist(entity);
			return entity;

		} catch (EntityExistsException ee) {
			ee.printStackTrace();
			throw new SystemException(CoreError.ERROR_DUPLICATE, ee, entity.getClass().getSimpleName());
		} catch (ConstraintViolationException cv) {
			cv.printStackTrace();
			throw new SystemException(CoreError.ERROR_DUPLICATE, cv, entity.getClass().getSimpleName());
		} catch (PropertyValueException pv) {
			pv.printStackTrace();
			throw new SystemException(CoreError.ERROR_UNDEFINED_PROPERTY, pv);
		} catch (Throwable t) {
			t.printStackTrace();
			throw new SystemException(CoreError.ERROR_SAVE, new Exception(t.getCause()));
		}
	}

	@Override
	@Interceptors({ LogInterceptor.class })
	public T update(T entity) throws SystemException {
		checkDuplicate(entity);
		try{
			return em.merge(entity);

		} catch (Throwable e) {
			e.printStackTrace();
			throw new SystemException(CoreError.ERROR_UPDATE, new Exception(e.getCause()));
		}
	}

	@Override
	@Interceptors({ LogInterceptor.class })
	public void delete(T entity) throws SystemException {
		try {
			em.remove(entity);

		} catch (ConstraintViolationException e) {
			e.printStackTrace();
			throw new SystemException(CoreError.ERROR_DELETE_RESTRICTION, e);
			
		} catch (Throwable e) {
			e.printStackTrace();
			throw new SystemException(CoreError.ERROR_DELETE, new Exception(e.getCause()));
		}
	}

	@Override
	public List<T> findAll() throws SystemException {
		CriteriaQuery cq = em.getCriteriaBuilder().createQuery();
		cq.select(cq.from(this.entity));
		return (List<T>) em.createQuery(cq).getResultList();
	}

	@Override
	public T findId(Long id) throws SystemException {
		return em.find(this.entity, id);
	}

	@Override
	public List<T> findNamedQuery(String namedQuery) throws SystemException {
		List<T> result = null;
		try {
			StringBuilder queryStr = new StringBuilder(getNamedQuery(namedQuery, entity));

			if (clauseWhere != null && clauseWhere.length() > 0) {
				StringBuilder append = new StringBuilder();
				if (!queryStr.toString().toLowerCase().contains(GenericDao.WHERE))
					append = new StringBuilder(" WHERE 1=1 ");
				queryStr.append(" ").append(append).append(clauseWhere);
			}

			Query query = em.createQuery(queryStr.toString());
			if (parameters != null && !parameters.isEmpty())
				for (Entry<String, Object> entry : parameters.entrySet())
					query.setParameter(entry.getKey(), entry.getValue());

			result = query.getResultList();

		} catch (Exception e) {
			throw new SystemException(CoreError.ERROR_GET_RECORD, e);
		}
		return result;
	}

	@Override
	public List<T> findParameters(int offset, int length, Map<String, Object> parameters, StringBuilder clauseWhere) throws SystemException {
		this.parameters = parameters;
		this.clauseWhere = clauseWhere;
		List<T> resultList = null;
		try{
			StringBuilder queryString = new StringBuilder(getNamedQuery(new StringBuilder(this.entity.getSimpleName()).append(".find").toString(), this.entity));
			if (clauseWhere != null && clauseWhere.length() > 0) {
				StringBuilder append = new StringBuilder();
				if (!queryString.toString().toLowerCase().contains(GenericDao.WHERE))
					append = new StringBuilder(" WHERE 1=1 ");
				queryString.append(" ").append(append).append(clauseWhere);
			}

			Query query = em.createQuery(queryString.toString());
			if(parameters != null && !parameters.isEmpty())
				for(Entry<String, Object> entry : parameters.entrySet())
					query.setParameter(entry.getKey(), entry.getValue());

			query.setFirstResult(offset).setMaxResults(length);
			resultList = query.getResultList();

		} catch (SystemException p) {
			p.printStackTrace();
			throw new SystemException(CoreError.ERROR_GET_RECORD, p);
		}
		return resultList;
	}

	public List<T> findExamples(T entity) throws SystemException {
		try{
			Example eg = Example.create(entity).excludeZeroes().ignoreCase().enableLike(MatchMode.ANYWHERE).excludeProperty("serialVersionUID");
			Session session = (Session) em.getDelegate();
			Criteria criteria = session.createCriteria(this.entity);
			return criteria.add(eg).list();
//			return criteria.add(eg).setFirstResult(3).setMaxResults(2).list();

		}catch (Throwable e) {
			e.printStackTrace();
			throw new SystemException(CoreError.ERROR_GET_RECORD, new Exception(e.getCause()));
		}
	}

	@Override
	public String getNamedQuery(String namedQuery, Class c) throws SystemException {
		try {
			NamedQueries namedQueries = (NamedQueries) c.getAnnotation(NamedQueries.class);
			NamedQuery[] queriesArr = namedQueries.value();
			for (NamedQuery n : queriesArr)
				if (n.name().equals(namedQuery))
					return n.query();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public void setEntity(Class<T> entity) {
		this.entity = entity;
	}

	@Override
	public int count(Map<String, Object> parameters, StringBuilder clauseWhere) throws SystemException {
		Number number = 0;
		try {
			StringBuilder queryStr = new StringBuilder(getNamedQuery(new StringBuilder(this.entity.getSimpleName()).append(".count").toString(), this.entity));

			if (clauseWhere != null && clauseWhere.length() > 0) {
				StringBuilder append = new StringBuilder();
				if (!queryStr.toString().toLowerCase().contains(GenericDao.WHERE))
					append = new StringBuilder(" WHERE 1=1 ");
				queryStr.append(" ").append(append).append(clauseWhere);
			}

			Query query = em.createQuery(queryStr.toString());
			if (parameters != null && !parameters.isEmpty())
				for (Entry<String, Object> entry : parameters.entrySet())
					query.setParameter(entry.getKey(), entry.getValue());

			number = (Number)query.getSingleResult();

		} catch (Exception e) {
			throw new SystemException(CoreError.ERROR_GET_RECORD, e);
		}
		return number == null ? 0 : number.intValue();
	}

	protected Object verifyId(Class<? super T> classe, T t) {
		if (classe != Object.class) {
			Field[] campos = classe.getDeclaredFields();
			for (Field field : campos) {
				if (field.isAnnotationPresent(Id.class)) {
					field.setAccessible(true);
					try {
						return field.get(t);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
		if (classe.getSuperclass() != Object.class) {
			return verifyId(classe.getSuperclass(), t);
		}
		return null;
	}

	private void checkDuplicate(T t) throws SystemException {
		StringBuilder queryString = new StringBuilder(getNamedQuery(getEntity().getSimpleName() + ".duplicate", getEntity()));
		if (queryString != null && !queryString.toString().trim().isEmpty()){
			if (verifyId(getEntity(), t) != null) {
				queryString = new StringBuilder(queryString.toString().toLowerCase().replaceAll("(?i)where ", "where ("));
				queryString.append(") and id!=:id");
			}

			Query query = em.createQuery(queryString.toString());
			Pattern pattern = Pattern.compile(":\\w+");
			Matcher matcher = pattern.matcher(queryString);
			while (matcher.find()) {
				String attribute = matcher.group();
				query.setParameter(attribute.substring(1), seachField(getEntity(), t, attribute.substring(1)));
			}

			if (query.getResultList() != null && !query.getResultList().isEmpty()) {
				Long total = (Long) query.getSingleResult();
				if (total > 0)
					throw new SystemException(CoreError.ERROR_DUPLICATE, t.getClass().getSimpleName());
			}
		}
	}

	protected Object seachField(Class<? super T> classe, T t, String nomeCampo) {
		if (classe != Object.class) {
			Field[] campos = classe.getDeclaredFields();
			for (Field field : campos) {
				if (field.getName().equals(nomeCampo)) {
					field.setAccessible(true);
					try {
						return field.get(t);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}

		if (classe.getSuperclass() != Object.class)
			return seachField(classe.getSuperclass(), t, nomeCampo);

		return null;
	}
	
	private String getId(Class<?> clazz, Object obj) throws SystemException {
		try{
			for (Field field : clazz.getDeclaredFields()) {
				if ((field.getAnnotation(Id.class)) != null) {
					Field privateField = clazz.getDeclaredField(field.getName());
					privateField.setAccessible(true);
					if (privateField.get(clazz.cast(obj)) != null)
						return (String) field.getType().cast(privateField.get(clazz.cast(obj))).toString();
					else
						return null;
				}
			}

		} catch (SecurityException | IllegalAccessException e) {
			e.printStackTrace();
			throw new SystemException(CoreError.ERROR_ILLEGAL_ACESS, e);
		} catch (NoSuchFieldException nsf) {
			nsf.printStackTrace();
			throw new SystemException(CoreError.ERROR_UNDEFINED_PROPERTY, nsf);
		} catch (IllegalArgumentException ia) {
			ia.printStackTrace();
			throw new SystemException(CoreError.ERROR_ILLEGAL_ARGUMENT, ia);
		}

		return null;
	}

	public Class<T> getEntity() {
		if (entity == null)
			this.entity = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
		return entity;
	}
}